/**
 * 
 */
package net.ib.mtalk.network;

import java.util.concurrent.atomic.AtomicInteger;

import net.ib.mntalk.protocol.Common;
import net.ib.mntalk.protocol.Common.Envelope;
import net.ib.mtalk.constant.MTalkConstant;
import net.ib.mtalk.util.MTalkLogWriter;
import android.os.SystemClock;

import com.google.protobuf.micro.ByteStringMicro;
import com.google.protobuf.micro.MessageMicro;

/**
 * {@link Envelope}에 대응되는 어플리케이션 수준 클래스. 객체를 새로 만드는 것은 {@link Builder TcpEnvelope.Builder}를 통해서.
 * @author Arngard
 */
public class TcpEnvelope {

	/**
	 * 값이 없거나 할당되지 않았음을 나타냄.
	 */
	public static final int NONE = -1;
	/**
	 * EnvelopeType.REQUEST 또는 {@link Common#REQUEST net.ib.mntalk.protocol.Common.REQUEST}와 호환
	 */
	public static final int REQUEST = net.ib.mntalk.protocol.Common.REQUEST;
	/**
	 * EnvelopeType.RESPONSE 또는 {@link Common#RESPONSE net.ib.mntalk.protocol.Common.RESPONSE}와 호환
	 */
	public static final int RESPONSE = net.ib.mntalk.protocol.Common.RESPONSE;
	
	private static volatile AtomicInteger requestIdGenerator = null;

	/*	Envelope 호환 정보	*/
	protected int requestId = NONE;	// 각 (req, res) 쌍이 갖는 고유 식별자
	protected int type = NONE;	// req 인지 res 인지 구분해줌. EnvelopeType
	protected int methodId = NONE;	// 어느 rpc인지
	protected MessageMicro message = null;	// 메시지 본체.
	/*	TcpEnvelope 부가 정보	*/
	protected NetResultReceiver callback = null;	// res가 어디로 가야 하는지
	protected long expirationStart;	// setExpireTime()이 호출된 시각을 기록하는 변수 
	protected long expirationLimit;	// setExpireTime()에서 인자로 받았던 유통기한
	
	/**
	 * @author Arngard
	 * @see TcpEnvelope
	 */
	public static class Builder {
		/*	Envelope 호환 정보	*/
		protected int requestId = NONE;	// 각 (req, res) 쌍이 갖는 고유 식별자
		protected int type = NONE;	// req 인지 res 인지 구분해줌. EnvelopeType
		protected int methodId = NONE;	// 어느 rpc인지
		protected MessageMicro message = null;	// 메시지 본체.
		/*	TcpEnvelope 부가 정보	*/
		protected NetResultReceiver callback = null;	// res가 어디로 가야 하는지
		protected long expirationStart;	// setExpireTime()이 호출된 시각을 기록하는 변수 
		protected long expirationLimit;	// setExpireTime()에서 인자로 받았던 유통기한

		{	// initialization block
			unsetExpireTime();
		}

		/**
		 * @param requestId 각 (req, res) 쌍이 갖는 고유 식별자. 전송 단계에서 네트워크 모듈에 의해 강제로 값이 할당됨.
		 * @return {@link Builder TcpEnvelope.Builder}
		 * @see TcpEnvelope#getNewRequestId()
		 */
		public Builder 	setRequestId(int requestId) {
			this.requestId = requestId;
			return this;
		}

		/**
		 * type req 인지 res 인지 구분해줌. EnvelopeType.
		 * @param {@link TcpEnvelope#REQUEST} or {@link TcpEnvelope#RESPONSE}.
		 * @return {@link Builder TcpEnvelope.Builder}
		 * @see TcpEnvelope
		 */
		public Builder setType(int type) {
			this.type = type;
			return this;
		}

		/**
		 * @param methodId 어느 rpc인지 구분
		 * @return {@link Builder TcpEnvelope.Builder}
		 * @see TcpEnvelope
		 * @see MethodId
		 */
		public Builder setMethodId(int methodId) {
			this.methodId = methodId;
			return this;
		}

		/**
		 * @param message 메시지 본체.
		 * @return {@link Builder TcpEnvelope.Builder}
		 * @see TcpEnvelope
		 */
		public Builder setMessage(MessageMicro message) {
			this.message = message;
			return this;
		}

		/**
		 * @param mCallback res가 어디로 가야 하는지
		 * @return {@link Builder TcpEnvelope.Builder}
		 * @see TcpEnvelope
		 */
		public Builder setCallback(NetResultReceiver callback) {
			this.callback = callback;
			return this;
		}
		
		/**
		 * 이 {@link TcpEnvelope}의 유통기한을 지정해준다.
		 * 네트워크 모듈은 전송 기록을 Lazy하게 불규칙적으로 검사한다.
		 * 검사중에 유통기한이 지난 요청을 발견하면 에러 콜백을 해 줄 것이다.<br>
		 * @param expirationLimitMillis milliseconds. 이 메소드를 호출한 시각 이후 언제까지 이 {@link TcpEnvelope}이 유효한가를 나타내는 유통기한.
		 * 절대시각이 아니다. {@link android.os.SystemClock#elapsedRealtime() SystemClock.elapsedRealtime()}과 같은 스케일의 상대기간.
		 * @return {@link Builder TcpEnvelope.Builder}
		 * @see TcpEnvelope
		 * @see TcpEnvelope.Builder#unsetExpireTime()
		 * @see TcpEnvelope#hasExpired()
		 */
		public Builder setExpireTime(long expirationLimitMillis) {
			expirationStart = android.os.SystemClock.elapsedRealtime();
			this.expirationLimit = expirationLimitMillis;
			return this;
		}
		
		/**
		 * setExpireTime()을 사용하지 않으면 이 함수가 기본으로 불린다.
		 * {@link TcpEnvelope.Builder#setExpireTime(long) setExpireTime(Long.MAX_VALUE)}와 같다.
		 * @return {@link Builder TcpEnvelope.Builder}
		 * @see TcpEnvelope
		 * @see TcpEnvelope.Builder#setExpireTime(long)
		 * @see TcpEnvelope#hasExpired()
		 */
		public Builder unsetExpireTime() {
			expirationStart = android.os.SystemClock.elapsedRealtime();
			expirationLimit = Long.MAX_VALUE;
			return this;
		}

		/**
		 * 빌더가 가진 정보를 이용하여 {@link TcpEnvelope} 객체를 구성함.
		 * @return 완성된 객체
		 */
		public TcpEnvelope build() {
			return new TcpEnvelope(this);
		}
	}
	
	protected TcpEnvelope(Builder builder) {
		this.requestId = builder.requestId;
		this.type = builder.type;
		this.methodId = builder.methodId;
		this.message = builder.message;
		
		if (MTalkConstant.LOG_MODE) {
			if (! MethodId.isRequiredFill(message)) {
				MTalkLogWriter.e("[LOG_MODE message] GPB Required field is not assigned in MessageMicro"
						+": requestId="+requestId
						+", type="+type
						+", methodId="+methodId);
			}
		}

		this.callback = builder.callback;
		this.expirationStart = builder.expirationStart;
		this.expirationLimit = builder.expirationLimit;
	}

	/**
	 * @return 각 (req, res) 쌍이 갖는 고유 식별자. 전송 단계에서 네트워크 모듈에 의해 강제로 값이 할당됨.
	 */
	public int getRequestId() {
		return requestId;
	}

	/**
	 * @param requestId 각 (req, res) 쌍이 갖는 고유 식별자
	 * @see TcpEnvelope#getNewRequestId()
	 */
	public void setRequestId(int requestId) {
		this.requestId = requestId;
	}

	/**
	 * req 인지 res 인지 구분해줌. EnvelopeType.
	 * @return {@link TcpEnvelope#REQUEST} or {@link TcpEnvelope#RESPONSE}.
	 */
	public int getType() {
		return type;
	}

	/**
	 * @return 어느 rpc인지 구분.
	 * @see MethodId
	 */
	public int getMethodId() {
		return methodId;
	}

	/**
	 * @return 메시지 본체
	 */
	public MessageMicro getMessage() {
		return message;
	}

	/**
	 * @return res가 어디로 가야 하는지
	 * @see {@link NetResultReceiver}
	 */
	public NetResultReceiver getCallback() {
		return callback;
	}
	
	/**
	 * Callback을 변경할 필요가 있을 때
	 * @param callback
	 */
	public void setCallback(NetResultReceiver callback){
		this.callback = callback;
	}
	
	public void passResult(Object sentObject, Object receivedObject) {
		if (callback == null)
			return;
		callback.passResult(NetResultReceiver.TCP_RESULT, sentObject, receivedObject);
	}

	public void passError(Object sentObject) {
		if (callback == null)
			return;
		callback.passError(NetResultReceiver.TCP_RESULT, sentObject);
	}
	
	/**
	 * {@link TcpEnvelope#hasExpired(long) hasExpired(SystemClock.elapsedRealtime())}와 같다.
	 * @return 유통기한이 지났으면 true
	 * @see TcpEnvelope.Builder#setExpireTime(long)
	 * @see TcpEnvelope.Builder#unsetExpireTime()
	 * @see {@link android.os.SystemClock#elapsedRealtime() SystemClock.elapsedRealtime()}
	 */
	public boolean hasExpired() {
		return hasExpired(SystemClock.elapsedRealtime());
	}

	/**
	 * @param elapsedRealtime 이 시각 기준으로 유통기한이 지났는지 체크한다.
	 * {@link android.os.SystemClock#elapsedRealtime() SystemClock.elapsedRealtime()}을 권장함.
	 * @return 유통기한이 지났으면 true
	 * @see TcpEnvelope.Builder#setExpireTime(long)
	 * @see TcpEnvelope.Builder#unsetExpireTime()
	 */
	public boolean hasExpired(long elapsedRealtime) {
		return expirationLimit < elapsedRealtime - expirationStart;
	}
	
	/*	Envelope 변환 시작	*/

	/**
	 * 이 Revelope이 가지고 있는 정보를 이용하여 새로운 {@link Envelope} 객체를 생성한다.
	 * @return 만들어진 Envelope. 정보가 부족하다면 null.
	 */
	public Envelope toEnvelope() {
		Envelope reVal = new Envelope();

		if (requestId == NONE) return null;
		reVal.setRequestId(requestId);

		if (type == NONE) return null;
		reVal.setType(type);

		if (methodId == NONE) return null;
		reVal.setMethodId(methodId);

		if (message == null) return null;
		reVal.setBody(ByteStringMicro.copyFrom(message.toByteArray()));
		
		return reVal;
	}

	/*	Envelope 변환 끝	*/

	/*	Request ID 관리 시작	*/
	
	/**
	 * {@link TcpEnvelope} 클래스가 관리하는 Request ID의 unique space를 리셋한다.
	 * @See {@link TcpEnvelope#getNewRequestId()}
	 */
	synchronized public static void resetRequestId() {
		if (requestIdGenerator == null)
			requestIdGenerator = new AtomicInteger();
		
		MTalkLogWriter.d("TcpEnvelope.resetRequestId()");
		requestIdGenerator.set(0);
	}

	/**
	 * 호출할 때마다 {@link TcpEnvelope} 클래스가 관리하는 Request ID의 unique space로부터 새로운 Request ID를 얻어온다. 이 값은 int 자료형 공간이 허용하는 한 unique하다.
	 * @return 새로운 Request ID.
	 * @See {@link TcpEnvelope#resetRequestId()}
	 */
	synchronized public static int getNewRequestId() {
		if (requestIdGenerator == null)
			requestIdGenerator = new AtomicInteger();
		
		MTalkLogWriter.v("TcpEnvelope.getNewRequestId()");
		return requestIdGenerator.getAndIncrement();
	}

	/*	Request ID 관리 끝	*/

}
